You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

644 lines
16 KiB

<script setup lang="ts">
import type { AgentTitleStrategy } from "#server/service/agent/agent";
definePageMeta({
layout: false,
});
const route = useRoute();
const router = useRouter();
const { $toast } = useNuxtApp() as any;
const slugParam = computed(() => String(route.params.slug));
const isCreate = computed(() => slugParam.value === "new");
interface ModelOption {
id: number;
name: string;
modelId: string;
providerName?: string;
providerId?: number;
supportsTools?: number;
}
interface ToolOption {
id: string;
slug: string;
name: string;
description: string;
}
const models = ref<ModelOption[]>([]);
const availableTools = ref<ToolOption[]>([]);
const form = reactive({
slug: "",
name: "",
description: "",
systemPrompt: "",
defaultModelId: null as number | null,
titleModelId: null as number | null,
titleStrategy: "llm" as AgentTitleStrategy,
enableThinking: 0,
enableTools: 1,
maxStepCount: 6,
isDefault: 0,
isCallable: 1,
sortOrder: 0,
enabled: 1,
toolIds: [] as string[],
});
const loading = ref(false);
const saving = ref(false);
async function loadModels() {
try {
const res = await $fetch<{ code: number; data: { list: any[] } }>("/api/llm/models");
const list = res.data?.list ?? [];
models.value = list.map((m) => ({
id: m.id,
name: m.name,
modelId: m.modelId,
providerName: m.providerName,
providerId: m.providerId,
supportsTools: m.supportsTools,
}));
} catch {
models.value = [];
}
}
async function loadTools() {
try {
const res = await $fetch<{ code: number; data: any[] }>("/api/agent-tools");
availableTools.value = (res.data ?? []).map((t: any) => ({
id: t.id,
slug: t.slug,
name: t.name,
description: t.description ?? "",
}));
} catch {
availableTools.value = [];
}
}
async function loadAgent() {
if (isCreate.value) return;
loading.value = true;
try {
const res = await $fetch<{ code: number; data: any }>(
`/api/admin/agents/${slugParam.value}`,
);
const d = res.data;
form.slug = d.slug ?? "";
form.name = d.name ?? "";
form.description = d.description ?? "";
form.systemPrompt = d.systemPrompt ?? "";
form.defaultModelId = d.defaultModelId ?? null;
form.titleModelId = d.titleModelId ?? null;
form.titleStrategy = (d.titleStrategy as AgentTitleStrategy) ?? "llm";
form.enableThinking = d.enableThinking ?? 0;
form.enableTools = d.enableTools ?? 1;
form.maxStepCount = d.maxStepCount ?? 6;
form.isDefault = d.isDefault ?? 0;
form.isCallable = d.isCallable ?? 1;
form.sortOrder = d.sortOrder ?? 0;
form.enabled = d.enabled ?? 1;
form.toolIds = (d.tools ?? []).map((t: any) => t.id);
} catch (e: any) {
$toast?.error?.(e?.data?.message ?? "加载 agent 失败");
router.push("/admin/agents");
} finally {
loading.value = false;
}
}
function toggleTool(id: string) {
const idx = form.toolIds.indexOf(id);
if (idx >= 0) {
form.toolIds.splice(idx, 1);
} else {
form.toolIds.push(id);
}
}
async function handleSave() {
if (!form.slug || !form.name || !form.systemPrompt) {
$toast?.error?.("slug、name、systemPrompt 为必填");
return;
}
saving.value = true;
try {
const body = { ...form };
if (isCreate.value) {
await $fetch("/api/agents", { method: "POST", body });
$toast?.success?.("创建成功");
} else {
await $fetch(`/api/agents/${slugParam.value}`, { method: "PUT", body });
$toast?.success?.("保存成功");
}
router.push("/admin/agents");
} catch (e: any) {
$toast?.error?.(e?.data?.message ?? "保存失败");
} finally {
saving.value = false;
}
}
onMounted(async () => {
await Promise.all([loadModels(), loadTools()]);
await loadAgent();
});
</script>
<template>
<div class="admin-agent-edit">
<header class="page-header">
<NuxtLink to="/admin/agents" class="back-link">← 返回列表</NuxtLink>
<h1 class="page-title">{{ isCreate ? '创建 Agent' : '编辑 Agent' }}</h1>
</header>
<div v-if="loading" class="loading-state">加载中...</div>
<template v-else>
<!-- 基本信息 -->
<section class="config-section">
<h2 class="section-title">基本信息</h2>
<p class="section-desc">Agent 的标识与显示名称</p>
<div class="form-grid">
<div class="form-field">
<label class="field-label">Slug <span class="required">*</span></label>
<input
v-model="form.slug"
type="text"
class="config-input"
placeholder="唯一标识,如 my-agent"
:disabled="!isCreate"
/>
</div>
<div class="form-field">
<label class="field-label">名称 <span class="required">*</span></label>
<input
v-model="form.name"
type="text"
class="config-input"
placeholder="显示名称"
/>
</div>
<div class="form-field form-field-full">
<label class="field-label">描述</label>
<input
v-model="form.description"
type="text"
class="config-input"
placeholder="简短描述"
/>
</div>
<div class="form-field">
<label class="field-label">排序</label>
<input
v-model.number="form.sortOrder"
type="number"
class="config-input"
placeholder="0"
/>
</div>
</div>
</section>
<!-- 系统提示词 -->
<section class="config-section">
<h2 class="section-title">系统提示词 <span class="required">*</span></h2>
<p class="section-desc">引导模型行为的系统提示词</p>
<textarea
v-model="form.systemPrompt"
class="config-textarea"
rows="8"
placeholder="输入系统提示词..."
/>
</section>
<!-- 模型配置 -->
<section class="config-section">
<h2 class="section-title">模型配置</h2>
<p class="section-desc">默认对话模型与标题生成模型</p>
<div class="model-row">
<div class="model-field">
<label class="field-label">默认模型</label>
<select v-model="form.defaultModelId" class="config-select">
<option :value="null">— 未设置 —</option>
<option v-for="m in models" :key="m.id" :value="m.id">
{{ m.name }} ({{ m.modelId }}) — {{ m.providerName || '未知' }}{{ m.supportsTools ? ' · 支持工具' : '' }}
</option>
</select>
</div>
<div class="model-field">
<label class="field-label">标题生成模型</label>
<select v-model="form.titleModelId" class="config-select">
<option :value="null">— 未设置 —</option>
<option v-for="m in models" :key="m.id" :value="m.id">
{{ m.name }} ({{ m.modelId }}) — {{ m.providerName || '未知' }}{{ m.supportsTools ? ' · 支持工具' : '' }}
</option>
</select>
</div>
</div>
<p v-if="models.length === 0" class="field-hint" style="color: var(--color-warning, #b06000);">
暂无可用模型,请先在 <NuxtLink to="/settings/llm-config" class="inline-link">LLM 配置</NuxtLink> 中添加 Provider 和模型
</p>
<div class="form-field" style="margin-top: 16px;">
<label class="field-label">标题策略</label>
<select v-model="form.titleStrategy" class="config-select">
<option value="llm">LLM 生成</option>
<option value="first-line">取首行</option>
<option value="none">不生成</option>
</select>
</div>
</section>
<!-- 行为配置 -->
<section class="config-section">
<h2 class="section-title">行为配置</h2>
<p class="section-desc">控制 Agent 的工具调用与思考能力</p>
<div class="toggle-grid">
<label class="toggle-item">
<input type="checkbox" :checked="form.enableTools === 1" @change="form.enableTools = form.enableTools ? 0 : 1" />
<div class="toggle-info">
<div class="toggle-name">启用工具</div>
<div class="toggle-desc">允许 Agent 调用绑定的工具</div>
</div>
</label>
<label class="toggle-item">
<input type="checkbox" :checked="form.enableThinking === 1" @change="form.enableThinking = form.enableThinking ? 0 : 1" />
<div class="toggle-info">
<div class="toggle-name">启用思考</div>
<div class="toggle-desc">允许模型使用思考模式</div>
</div>
</label>
<label class="toggle-item">
<input type="checkbox" :checked="form.isCallable === 1" @change="form.isCallable = form.isCallable ? 0 : 1" />
<div class="toggle-info">
<div class="toggle-name">可调用</div>
<div class="toggle-desc">在聊天页可选列表中显示</div>
</div>
</label>
<label class="toggle-item">
<input type="checkbox" disabled :checked="form.isDefault === 1" @change="form.isDefault = form.isDefault ? 0 : 1" />
<div class="toggle-info">
<div class="toggle-name">默认 Agent</div>
<div class="toggle-desc">设为新用户/未指定时的默认 Agent</div>
</div>
</label>
<label class="toggle-item">
<input type="checkbox" :checked="form.enabled === 1" @change="form.enabled = form.enabled ? 0 : 1" />
<div class="toggle-info">
<div class="toggle-name">启用</div>
<div class="toggle-desc">Agent 是否对用户可见</div>
</div>
</label>
</div>
<div class="form-field" style="margin-top: 16px;">
<label class="field-label">最大步数</label>
<input
v-model.number="form.maxStepCount"
type="number"
min="1"
max="50"
class="config-input"
style="width: 120px;"
/>
</div>
</section>
<!-- 工具绑定 -->
<section class="config-section">
<h2 class="section-title">工具绑定</h2>
<p class="section-desc">勾选此 Agent 可使用的工具(按勾选顺序提交)</p>
<div class="tools-list">
<label
v-for="t in availableTools"
:key="t.id"
class="tool-item"
>
<input
type="checkbox"
:checked="form.toolIds.includes(t.id)"
@change="toggleTool(t.id)"
/>
<div class="tool-info">
<div class="tool-name">{{ t.name }}</div>
<div class="tool-desc">{{ t.description }}</div>
</div>
</label>
<div v-if="availableTools.length === 0" class="empty-tools">
暂无可用工具
</div>
</div>
</section>
<!-- 操作按钮 -->
<div class="action-bar">
<NuxtLink to="/admin/agents" class="btn-cancel">取消</NuxtLink>
<button class="save-btn" :disabled="saving" @click="handleSave">
{{ saving ? '保存中...' : (isCreate ? '创建' : '保存') }}
</button>
</div>
</template>
</div>
</template>
<style scoped>
.admin-agent-edit {
max-width: 800px;
margin: 0 auto;
padding: 32px 24px;
}
.page-header {
margin-bottom: 32px;
}
.back-link {
display: inline-block;
font-size: 13px;
color: var(--color-muted);
text-decoration: none;
margin-bottom: 8px;
}
.back-link:hover {
color: var(--color-primary);
}
.inline-link {
color: var(--color-primary);
text-decoration: underline;
}
.field-hint {
font-size: 13px;
margin-top: 8px;
}
.page-title {
font-family: var(--font-display);
font-size: 28px;
font-weight: 400;
color: var(--color-ink);
margin: 0;
}
.loading-state {
text-align: center;
padding: 48px;
color: var(--color-muted);
}
.config-section {
margin-bottom: 32px;
padding: 24px;
background: var(--color-surface-card);
border: 1px solid var(--color-hairline);
border-radius: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--color-ink);
margin: 0 0 4px;
}
.section-desc {
font-size: 13px;
color: var(--color-muted);
margin: 0 0 16px;
}
.required {
color: #e53e3e;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-field-full {
grid-column: 1 / -1;
}
.field-label {
font-size: 13px;
font-weight: 500;
color: var(--color-muted);
}
.config-input {
padding: 8px 12px;
border: 1px solid var(--color-hairline);
border-radius: 8px;
font-size: 14px;
color: var(--color-ink);
background: var(--color-canvas);
outline: none;
transition: border-color 0.15s;
}
.config-input:focus {
border-color: var(--color-primary);
}
.config-input:disabled {
background: var(--color-surface-soft);
color: var(--color-muted);
cursor: not-allowed;
}
.config-textarea {
width: 100%;
padding: 12px 14px;
border: 1px solid var(--color-hairline);
border-radius: 10px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 13px;
line-height: 1.6;
color: var(--color-ink);
background: var(--color-canvas);
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
.config-textarea:focus {
border-color: var(--color-primary);
}
.config-select {
padding: 8px 12px;
border: 1px solid var(--color-hairline);
border-radius: 8px;
font-size: 14px;
color: var(--color-ink);
background: var(--color-canvas);
outline: none;
cursor: pointer;
}
.model-row {
display: flex;
gap: 16px;
}
.model-field {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.toggle-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.toggle-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
background: var(--color-surface-soft);
border: 1px solid var(--color-hairline);
border-radius: 10px;
cursor: pointer;
transition: border-color 0.15s;
}
.toggle-item:hover {
border-color: var(--color-primary);
}
.toggle-item input[type="checkbox"] {
margin-top: 2px;
accent-color: var(--color-primary);
}
.toggle-info {
flex: 1;
}
.toggle-name {
font-size: 14px;
font-weight: 500;
color: var(--color-ink);
}
.toggle-desc {
font-size: 12px;
color: var(--color-muted);
margin-top: 2px;
}
.tools-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.tool-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
background: var(--color-surface-soft);
border: 1px solid var(--color-hairline);
border-radius: 10px;
cursor: pointer;
transition: border-color 0.15s;
}
.tool-item:hover {
border-color: var(--color-primary);
}
.tool-item input[type="checkbox"] {
margin-top: 2px;
accent-color: var(--color-primary);
}
.tool-info {
flex: 1;
}
.tool-name {
font-size: 14px;
font-weight: 500;
color: var(--color-ink);
}
.tool-desc {
font-size: 12px;
color: var(--color-muted);
margin-top: 2px;
}
.empty-tools {
padding: 24px;
text-align: center;
font-size: 13px;
color: var(--color-muted);
}
.action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
.btn-cancel {
padding: 8px 20px;
background: var(--color-surface-card);
color: var(--color-body-strong);
border: 1px solid var(--color-hairline);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.15s;
}
.btn-cancel:hover {
background: var(--color-surface-soft);
}
.save-btn {
padding: 8px 24px;
background: var(--color-primary);
color: var(--color-on-primary);
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
.save-btn:hover:not(:disabled) {
background: var(--color-primary-active);
}
.save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>